﻿using System.Collections.Generic;
using System.Threading;
using static Vanara.PInvoke.Kernel32;
using static Vanara.PInvoke.User32;

namespace Vanara.PInvoke;

/// <summary>
/// An event handler that is dependent on window messages. This class works on both windowed and console applications, creating a
/// threaded message pump if needed.
/// <para>
/// To use, derive a class and override the <see cref="MessageFilter"/> method. When handling the message, use the <see
/// cref="RaiseEvent(Guid, object[])"/> method to call the event.
/// </para>
/// <para>
/// Delegates can be registered and unregistered using unique GUID values via the <see cref="AddEvent(Guid, Delegate)"/> and <see
/// cref="RemoveEvent(Guid, Delegate)"/> methods.
/// </para>
/// </summary>
/// <seealso cref="IDisposable"/>
public abstract class SystemEventHandler : IDisposable
{
	private static ManualResetEvent? eventWindowReady;
	private static Thread? windowThread;
	private readonly Dictionary<Guid, Delegate> eventHandles = new();
	private readonly object lockObj = new();
	private bool disposedValue;
	private BasicMessageWindow? msgWindow;

	/// <summary>Initializes a new instance of the <see cref="SystemEventHandler"/> class.</summary>
	/// <param name="forceThread">
	/// if set to <see langword="true"/> a new thread is created for the message pump regardless of the current apartment state.
	/// </param>
	/// <exception cref="InvalidOperationException">System events not supported.</exception>
	protected SystemEventHandler(bool forceThread = false)
	{
		if (Thread.GetDomain().GetData(".appDomain") != null)
			throw new InvalidOperationException("System events not supported.");

		if (!forceThread && !UserSession.IsInteractive || Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
		{
			Init();
		}
		else
		{
			ThreadRunning = new ManualResetEvent(false);
			eventWindowReady = new ManualResetEvent(false);
			windowThread = new Thread(MTAThreadProc) { IsBackground = true, Name = typeof(SystemEventHandler).FullName };
			windowThread.Start(this);
			eventWindowReady.WaitOne();
		}
	}

	/// <summary>Finalizes an instance of the <see cref="SystemEventHandler"/> class.</summary>
	~SystemEventHandler()
	{
		Dispose(false);
	}

	/// <summary>Occurs when the message window handle has been created.</summary>
	public event EventHandler? MessageWindowHandleCreated;

	/// <summary>Gets a value indicating whether this instance is running in a thread.</summary>
	/// <value><see langword="true"/> if this instance is running in a thread; otherwise, <see langword="false"/>.</value>
	public bool IsRunningInThread => ThreadRunning is not null;

	/// <summary>Gets the message window handle which can be used to register for messaged events.</summary>
	/// <value>The message window handle.</value>
	public HWND MessageWindowHandle => msgWindow?.Handle ?? HWND.NULL;

	private ManualResetEvent? ThreadRunning { get; set; }

	/// <summary>Adds a delegate and its associated key to the handler list.</summary>
	/// <param name="key">The key.</param>
	/// <param name="value">The delegate value.</param>
	public void AddEvent(Guid key, Delegate value)
	{
		lock (lockObj)
		{
			if (!eventHandles.TryGetValue(key, out Delegate? h))
			{
				eventHandles.Add(key, value);
				OnEventAdd(key);
			}
			else
			{
				eventHandles[key] = Delegate.Combine(h, value);
			}
		}
	}

	/// <summary>Removes a delegate and its associated key to the handler list.</summary>
	/// <param name="key">The key.</param>
	/// <param name="value">The delegate value.</param>
	public void RemoveEvent(Guid key, Delegate value)
	{
		lock (lockObj)
		{
			if (eventHandles.TryGetValue(key, out Delegate? h))
			{
				h = Delegate.Remove(h, value);
				if (h is null || h.GetInvocationList().Length == 0)
				{
					eventHandles.Remove(key);
					OnEventRemove(key);
				}
				else
				{
					eventHandles[key] = h;
				}
			}
			else
			{
				OnEventRemove(key);
			}
		}
	}

	/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
	void IDisposable.Dispose()
	{
		Dispose(true);
		System.GC.SuppressFinalize(this);
	}

	/// <summary>Releases unmanaged and - optionally - managed resources.</summary>
	/// <param name="disposing">
	/// <see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.
	/// </param>
	protected virtual void Dispose(bool disposing)
	{
		if (!disposedValue)
		{
			if (disposing)
			{
				if (ThreadRunning != null && IsWindow(MessageWindowHandle))
				{
					PostMessage(MessageWindowHandle, (uint)WindowMessage.WM_QUIT);
					ThreadRunning.WaitOne(5000);
					ThreadRunning = null;
				}
			}

			msgWindow?.Dispose();

			disposedValue = true;
		}
	}

	/// <summary>Determines whether the specified key has a delegate handler in its list.</summary>
	/// <param name="key">The key.</param>
	/// <returns><see langword="true"/> if the specified key exists; otherwise, <see langword="false"/>.</returns>
	protected bool HasKey(Guid key)
	{
		lock (lockObj)
			return eventHandles.ContainsKey(key);
	}

	/// <summary>Provides access to the WndProc listening for messages.</summary>
	/// <param name="hwnd">A handle to the window procedure that received the message.</param>
	/// <param name="msg">The message.</param>
	/// <param name="wParam">Additional message information. The content of this parameter depends on the value of the Msg parameter.</param>
	/// <param name="lParam">Additional message information. The content of this parameter depends on the value of the Msg parameter.</param>
	/// <param name="lReturn">The return value is the result of the message processing and depends on the message.</param>
	/// <returns>
	/// <see langword="true"/> if this message should be considered handled; or <see langword="false"/> to pass the message along to
	/// <see cref="DefWindowProc"/>.
	/// </returns>
	protected abstract bool MessageFilter(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam, out IntPtr lReturn);

	/// <summary>
	/// Allows derived classes to pre-process a message in a threaded message pump so that it is not passed along. Useful for things
	/// like <see cref="IsDialogMessage"/>.
	/// </summary>
	/// <param name="msg">The MSG.</param>
	/// <returns>
	/// <see langword="true"/> if the message was processed and should not be translated and dispatched; <see langword="false"/>
	/// otherwise. <see langword="false"/> is the default.
	/// </returns>
	protected virtual bool PreprocessMessage(in MSG msg) => false;

	/// <summary>Called when an event has been added.</summary>
	/// <param name="key">The event key.</param>
	protected virtual void OnEventAdd(Guid key)
	{
	}

	/// <summary>Called when an event has been removed.</summary>
	/// <param name="key">The event key.</param>
	protected virtual void OnEventRemove(Guid key)
	{
	}

	/// <summary>Called when the message window handle is created.</summary>
	protected virtual void OnMessageWindowHandleCreated() => MessageWindowHandleCreated?.Invoke(this, EventArgs.Empty);

	/// <summary>Calls the delegate list associated with the key, passing the supplied parameters.</summary>
	/// <param name="key">The key.</param>
	/// <param name="args">The arguments.</param>
	/// <returns>The value returned by the call to the delegate list.</returns>
	/// <exception cref="InvalidOperationException">Event for {key} is not registered.</exception>
	protected object? RaiseEvent(Guid key, params object?[] args)
	{
		Delegate? h;
		lock (lockObj)
		{
			if (!eventHandles.TryGetValue(key, out h))
				throw new InvalidOperationException($"Event for {key} is not registered.");
		}
		return h?.DynamicInvoke(args);
	}

	private static void MTAThreadProc(object? param)
	{
		if (param is not SystemEventHandler handler)
			throw new InvalidOperationException("Invalid parameter type.");
		try
		{
			handler.Init();
			eventWindowReady?.Set();
			if (!handler.MessageWindowHandle.IsNull)
			{
				var keepRunning = true;
				while (keepRunning)
				{
					var ret = MsgWaitForMultipleObjectsEx(0, null, 100, QS.QS_ALLINPUT, MWMO.MWMO_INPUTAVAILABLE);
					if (ret == (uint)WAIT_STATUS.WAIT_TIMEOUT)
					{
						Thread.Sleep(1);
					}
					else
					{
						while (PeekMessage(out MSG msg, wRemoveMsg: PM.PM_REMOVE))
						{
							if (msg.message == (uint)WindowMessage.WM_QUIT)
							{
								keepRunning = false;
								break;
							}
							if (!handler.PreprocessMessage(msg))
							{
								TranslateMessage(msg);
								DispatchMessage(msg);
							}
						}
					}
				}
			}
		}
		catch (Exception e)
		{
			eventWindowReady?.Set();
			if (e is not (ThreadInterruptedException or ThreadAbortException))
				System.Diagnostics.Debug.Fail("Unexpected thread exception in SystemEventHandler thread.", e.ToString());
		}

		handler.ThreadRunning?.Set();
	}

	private void Init()
	{
		msgWindow = new BasicMessageWindow(MessageFilter);
		OnMessageWindowHandleCreated();
	}
}

internal static class UserSession
{
	private static bool isUserInteractive;
	private static HWINSTA processWinStation;

	public static bool IsInteractive
	{
		get
		{
			if (Environment.OSVersion.Platform == PlatformID.Win32NT)
			{
				HWINSTA hwinsta = GetProcessWindowStation();
				if (!hwinsta.IsNull && processWinStation != hwinsta)
				{
					using var flags = new SafeCoTaskMemStruct<USEROBJECTFLAGS>();
					isUserInteractive = !GetUserObjectInformation((IntPtr)hwinsta, UserObjectInformationType.UOI_FLAGS, flags, flags.Size, out _) || (flags.Value.dwFlags & 0x0001 /*WSF_VISIBLE*/) != 0;
					processWinStation = hwinsta;
				}
			}
			else
			{
				isUserInteractive = true;
			}
			return isUserInteractive;
		}
	}
}